/*
 * Copyright (c) 2007 jNetX.
 * http://www.jnetx.com
 * All rights reserved.
 *
 * This software is the confidential and proprietary information of
 * jNetX. You shall not disclose such Confidential Information and
 * shall use it only in accordance with the terms of the license
 * agreement you entered into with jNetX.
 *
 * $Id$
 */
package com.jnetx.javax.sip.message;

import com.jnetx.javax.sip.header.ContentLengthHeaderImpl;
import com.jnetx.javax.sip.header.HeaderBase;
import com.jnetx.javax.sip.util.ByteData;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import javax.sip.InvalidArgumentException;
import javax.sip.SipException;
import javax.sip.header.*;
import javax.sip.message.Message;
import java.lang.reflect.Method;
import java.text.ParseException;
import java.util.*;

/**
 * @author <a href="mailto:dparhonin@jnetx.ru">Dmitry Parhonin</a>
 * @version $Revision$
 */
public abstract class SIPMessage implements Message {
    private static final Log logger = LogFactory.getLog(SIPMessage.class);

    private final Map<String, LinkedList<Header>> headers;
    private final List<String> headerNames;
    private List<ByteData> unrecognizedHeaders = null;
    private List<String> unrecognizedHeadersAsStrings = null;

    private String sipVersion = "2.0";
    private Object content;

    protected byte[] messageData;
    protected boolean parsed = true;

    public SIPMessage() {
        headers = new HashMap<String, LinkedList<Header>>();
        headerNames = new ArrayList<String>();
    }

    protected SIPMessage(int numberOfHeaderNames) {
        headers = new HashMap<String, LinkedList<Header>>(numberOfHeaderNames);
        headerNames = new ArrayList<String>(numberOfHeaderNames);
    }

    protected final int getNumberOfHeaderNames() {
        if (!parsed) parse();
        return headerNames.size();
    }

    protected final LinkedList<Header> retrieveHeaderList(String headerName) {
        if (!parsed) parse();
        LinkedList<Header> list = headers.get(headerName);
        if (list == null) {
            list = new LinkedList<Header>();
            headers.put(headerName, list);
            headerNames.add(headerName);
        }
        return list;
    }

    void addUnrecognizedHeader(ByteData unrecognizedHeaderData) {
        if (this.unrecognizedHeaders == null)
            this.unrecognizedHeaders = new ArrayList<ByteData>();
        this.unrecognizedHeaders.add(unrecognizedHeaderData);
    }

    /**
     * Adds the new Header to the existing list of Headers contained in this
     * Message. The Header is added to the end of the List and will appear in
     * that order in the SIP Message.
     * <p/>
     * Required Headers that are singletons should not be added to the message
     * as they already exist in the message and therefore should be changed using
     * the {@link javax.sip.message.Message#setHeader(javax.sip.header.Header)} method.
     * <p/>
     * This method should be used to support the special case of adding
     * required ViaHeaders to a message. When adding a ViaHeader using this
     * method the implementation will add the ViaHeader to the top of the
     * ViaHeader list, and not the end like all other Headers.
     *
     * @param header the new Header to be added to the existing Headers List.
     */
    public void addHeader(Header header) {
        if (!parsed) parse();
        retrieveHeaderList(header.getName()).add(header);
        ((HeaderBase) header).setOwner(this);
        invalidateMessageData();
    }

    /**
     * Adds the new Header to the end of existing list of Headers contained in this
     * Message. The Header is added to the end of the List and will appear in
     * that order in the SIP Message.
     * <p/>
     * Required Headers that are singletons should not be added to the message
     * as they already exist in the message and therefore should be changed using
     * the {@link javax.sip.message.Message#setHeader(javax.sip.header.Header)} method. This does the same thing
     * as addHeader in all cases including the Via header.
     * Add a header explicitly to the end of a list of headers.
     *
     * @param header the new Header to be added to the end of the existing
     *               list of headers
     * @throws NullPointerException   if the  argument is null.
     * @throws javax.sip.SipException if the header is a singleton and an instance of the header
     *                                already exists.
     */
    public void addLast(Header header) throws SipException, NullPointerException {
        if (!parsed) parse();
        retrieveHeaderList(header.getName()).addLast(header);
        invalidateMessageData();
    }

    /**
     * Adds the new Header to the head of the existing list of Headers
     * contained in this Message.
     * The Header is added to the  head of the List and will appear in
     * that order in the SIP Message.
     * <p/>
     * Required Headers that are singletons should not be added to the message
     * as they already exist in the message and therefore should be
     * changed using the {@link javax.sip.message.Message#setHeader(javax.sip.header.Header)} method.
     *
     * @param header the new Header to be added to the existing Headers List.
     * @throws javax.sip.SipException if the header to be added is a singleton and an instance of the header
     *                                already exists.
     * @throws NullPointerException   if the  argument is null.
     */
    public void addFirst(Header header) throws SipException, NullPointerException {
        if (!parsed) parse();
        retrieveHeaderList(header.getName()).addFirst(header);
        invalidateMessageData();
    }

    /**
     * Removes the first header from a list of headers.
     * If there's only one header of this kind, then it is removed
     * from the message.
     *
     * @param headerName the name of the header to be removed.
     * @throws NullPointerException if the arg is null
     */
    public void removeFirst(String headerName) throws NullPointerException {
        if (!parsed) parse();
        final LinkedList<Header> list = headers.get(headerName);
        if (list != null) {
            if (list.size() > 1)
                list.removeFirst();
            else {
                headers.remove(headerName);
                headerNames.remove(headerName);
            }
        }
        invalidateMessageData();
    }

    /**
     * Removes the last header from a list of headers.
     * If there's only one header of this kind, then it is removed
     * from the message.
     *
     * @param headerName the name of the header to be removed.
     */
    public void removeLast(String headerName) throws NullPointerException {
        if (!parsed) parse();
        final LinkedList<Header> list = headers.get(headerName);
        if (list != null) {
            if (list.size() > 1)
                list.removeLast();
            else {
                headers.remove(headerName);
                headerNames.remove(headerName);
            }
        }
        invalidateMessageData();
    }

    /**
     * Removes the Header of the supplied name from the list of headers in
     * this Message. If multiple headers exist then they are all removed from
     * the header list. If no headers exist then this method returns silently.
     * This method should not be used to remove required Headers, required
     * Headers should be replaced using the {@link javax.sip.message.Message#setHeader(javax.sip.header.Header)}.
     *
     * @param headerName the new string value name of the Header to be
     *                   removed.
     */
    public void removeHeader(String headerName) {
        if (!parsed) parse();
        final LinkedList<Header> list = headers.get(headerName);
        if (list != null) {
            headers.remove(headerName);
            headerNames.remove(headerName);
        }
        invalidateMessageData();
    }

    /**
     * Gets a ListIterator over the set of all all the header names in this Message.
     * Note that the order of the Header Names in the ListIterator is same as the
     * order in which they appear in the SIP Message.
     *
     * @return the ListIterator over the set of all the Header Names in the Message.
     */
    public ListIterator getHeaderNames() {
        if (!parsed) parse();
        return headerNames.listIterator();
    }

    /**
     * Gets a ListIterator over all the Headers of the newly specified name
     * in this Message. Note that order of the Headers in ListIterator is the
     * same as the order in which they appear in the SIP Message.
     *
     * @param headerName the new string name of Header types requested.
     * @return the ListIterator over all the Headers of the specified name in
     *         the Message, this method returns an empty ListIterator if no Headers
     *         exist of this header type.
     */
    public ListIterator getHeaders(String headerName) {
        if (!parsed) parse();
        List<Header> headerList = retrieveHeaderList(headerName);
        Header[] headers = headerList.toArray(new Header[headerList.size()]);
        for (Header header : headers)
            ((HeaderBase) header).parse();
        return retrieveHeaderList(headerName).listIterator();
    }

    /**
     * Gets the Header of the specified name in this Message. If multiple
     * Headers of this header name exist in the message, the first header
     * in the message is returned.
     *
     * @param headerName the new string name of Header type requested.
     * @return the Header of the specified name in the Message, this method
     *         returns null if the Header does not exist.
     */
    public Header getHeader(String headerName) {
        if (!parsed) parse();
        final LinkedList<Header> list = retrieveHeaderList(headerName);
        if (list.isEmpty())
            return null;
        else
            return list.getFirst();
    }

    /**
     * Returns a ListIterator over all the UnrecognizedHeaders in this Message. Note
     * the order of the UnrecognizedHeaders in the ListIterator is the same as
     * order in which they appeared in the SIP Message. UnrecognizedHeaders are
     * headers that the underlying implementation does not recognize.  If the
     * message is missing a required header (From, To, Call-ID, CSeq, Via)
     * the entire message will be dropped by the underlying implementation and
     * the header will not be included in the list. Headers that are part of the
     * supported set of headers but are not properly formatted will  be included
     * in this list. Note that Headers that are not part of the supported set of
     * headers are retrieved as Extension Headers. These must have a name:value
     * format else they will be rejected by the underling implementation and
     * included in this list. A Proxy should not delete UnrecognizedHeaders and
     * should add these Headers to the end of the header list of the Message
     * that is being forwarded. A User Agent may display these unrecognized
     * headers to the user.
     *
     * @return the ListIterator over all the UnrecognizedHeaders in the Message
     *         represented as Strings, this method returns an empty ListIterator if no
     *         UnrecognizedHeaders exist.
     */
    public ListIterator getUnrecognizedHeaders() {
        if (!parsed) parse();
        if (unrecognizedHeaders != null) {
            if (unrecognizedHeadersAsStrings == null || unrecognizedHeadersAsStrings.size() != unrecognizedHeaders.size()) {
                unrecognizedHeadersAsStrings = new ArrayList<String>(unrecognizedHeaders.size());
                for (ByteData data : unrecognizedHeaders)
                    unrecognizedHeadersAsStrings.add(data.asString());
            }
            return unrecognizedHeadersAsStrings.listIterator();
        } else
            return Collections.EMPTY_LIST.listIterator();
    }

    /**
     * Sets the new Header to replace existings Header of that type in
     * the message. If the SIP message contains more than one Header of
     * the new Header type it should replace the first occurance of this
     * Header and removes all other Headers of this type. If no Header of this
     * type exists this header is added to the end of the SIP Message.
     * This method should be used to change required Headers and overwrite
     * optional Headers.
     *
     * @param header the new Header to replace any existing Headers of that
     *               type.
     */
    public void setHeader(Header header) {
        if (!parsed) parse();
        final LinkedList<Header> list = headers.get(header.getName());
        list.clear();
        list.add(header);
        invalidateMessageData();
    }

    /**
     * Set the ContentLengthHeader of this Message.
     * The actual content length for the outgoing message will be computed from
     * the content assigned. If the content is speficied as an object it will
     * be converted to a String before the message is sent out and the content
     * length computed from the length of the string. If the message content is
     * specified in bytes, the length of the byte array specified will be used
     * to determine the content length header, that is in both cases, the length
     * of the content overrides any value specified in the content-length
     * header.
     *
     * @param contentLength the new ContentLengthHeader object containing the
     *                      content length value of this Message.
     */
    public void setContentLength(ContentLengthHeader contentLength) {
        if (!parsed) parse();
        setHeader(contentLength);
        invalidateMessageData();
    }

    /**
     * Gets the ContentLengthHeader of the body content of this Message. This is
     * the same as <code>this.getHeader(Content-Length);</code>
     *
     * @return the ContentLengthHeader of the message body.
     */
    public ContentLengthHeader getContentLength() {
        if (!parsed) parse();
        return (ContentLengthHeader) getHeader(ContentLengthHeader.NAME);
    }

    /**
     * Sets the ContentLanguageHeader of this Message. This overrides the
     * ContentLanguageHeader set using the setHeaders method. If no
     * ContentLanguageHeader exists in this message this ContentLanguageHeader
     * is added to the end of the Header List.
     *
     * @param contentLanguage the new ContentLanguageHeader object containing the
     *                        content language value of this Message.
     */
    public void setContentLanguage(ContentLanguageHeader contentLanguage) {
        if (!parsed) parse();
        setHeader(contentLanguage);
        invalidateMessageData();
    }

    /**
     * Gets the ContentLanguageHeader of this Message. This is the same as
     * <code>this.getHeader(Content-Langauge);</code>
     *
     * @return the ContentLanguageHeader of the message body.
     */
    public ContentLanguageHeader getContentLanguage() {
        if (!parsed) parse();
        return (ContentLanguageHeader) getHeader(ContentLanguageHeader.NAME);
    }

    /**
     * Sets the ContentEncodingHeader of this Message. This overrides the
     * ContentEncodingHeader set using the setHeaders method. If no
     * ContentEncodingHeader exists in this message this ContentEncodingHeader
     * is added to the end of the Header List.
     *
     * @param contentEncoding the new ContentEncodingHeader object containing the
     *                        content encoding values of this Message.
     */
    public void setContentEncoding(ContentEncodingHeader contentEncoding) {
        if (!parsed) parse();
        setHeader(contentEncoding);
        invalidateMessageData();
    }

    /**
     * Gets the ContentEncodingHeader of this Message. This is the same as
     * <code>this.getHeader(Content-Encoding);</code>
     *
     * @return the ContentEncodingHeader of the message body.
     */
    public ContentEncodingHeader getContentEncoding() {
        if (!parsed) parse();
        return (ContentEncodingHeader) getHeader(ContentEncodingHeader.NAME);
    }

    /**
     * Sets the ContentDispositionHeader of this Message. This overrides the
     * ContentDispositionHeader set using the setHeaders method. If no
     * ContentDispositionHeader exists in this message this ContentDispositionHeader
     * is added to the end of the Header List.
     *
     * @param contentDisposition the new ContentDispositionHeader object
     *                           containing the content disposition value of this Message.
     */
    public void setContentDisposition(ContentDispositionHeader contentDisposition) {
        if (!parsed) parse();
        setHeader(contentDisposition);
        invalidateMessageData();
    }

    /**
     * Gets the ContentDispositionHeader of this Message. This is the same as
     * <code>this.getHeader(Content-Disposition);</code>
     *
     * @return the ContentDispositionHeader of the message body.
     */
    public ContentDispositionHeader getContentDisposition() {
        if (!parsed) parse();
        return (ContentDispositionHeader) getHeader(ContentDispositionHeader.NAME);
    }

    /**
     * Sets the body of this Message, with the ContentType defined by the new
     * ContentTypeHeader object and the string value of the content.
     *
     * @param content           the new Object value of the content of the Message.
     * @param contentTypeHeader the new ContentTypeHeader object that defines
     *                          the content type value.
     * @throws java.text.ParseException which signals that an error has been reached
     *                                  unexpectedly while parsing the body.
     */
    public void setContent(Object content, ContentTypeHeader contentTypeHeader) throws ParseException {
        if (!parsed) parse();
        this.content = content;
        setHeader(contentTypeHeader);
        invalidateMessageData();
    }

    /**
     * Gets the body content of the Message as a byte array.
     *
     * @return the body content of the Message as a byte array, this method
     *         returns null if a body does not exist.
     */
    public byte[] getRawContent() {
        if (!parsed) parse();
        return content != null ? content.toString().getBytes() : null;
    }

    /**
     * Gets the body content of the Message as an Object.
     *
     * @return the body content of the Message as an Object, this method
     *         returns null if a body does not exist.
     */
    public Object getContent() {
        if (!parsed) parse();
        return content;
    }

    /**
     * Removes the body content from this Message and all associated entity
     * headers, if a body exists, this method returns sliently if no body exists.
     */
    public void removeContent() {
        if (!parsed) parse();
        this.content = null;
        removeHeader(ContentTypeHeader.NAME);
        invalidateMessageData();
    }

    /**
     * Sets the ExpiresHeader of this Message. This overrides the ExpiresHeader
     * set using the setHeaders method. If no ExpiresHeader exists in this
     * message this ExpiresHeader is added to the end of the Header List.
     *
     * @param expires the new ExpiresHeader object containing the expires
     *                values of this Message.
     */
    public void setExpires(ExpiresHeader expires) {
        if (!parsed) parse();
        setHeader(expires);
        invalidateMessageData();
    }

    /**
     * Gets the ExpiresHeader of this Message. This is the same as
     * <code>this.getHeader(Expires);</code>
     *
     * @return the ExpiresHeader of the message body.
     */
    public ExpiresHeader getExpires() {
        if (!parsed) parse();
        return (ExpiresHeader) getHeader(ExpiresHeader.NAME);
    }

    /**
     * Sets the protocol version of SIP being used by this Message.
     *
     * @param version the new String object containing the version of the SIP
     *                Protocol of this Message.
     * @throws java.text.ParseException which signals that an error has been reached
     *                                  unexpectedly while parsing the version argument.
     */
    public void setSIPVersion(String version) throws ParseException {
        if (!parsed) parse();
        this.sipVersion = version;
        invalidateMessageData();
    }

    /**
     * Gets the protocol version of SIP being used by this Message.
     *
     * @return the protocol version of the SIP protocol of this message.
     */
    public String getSIPVersion() {
        if (!parsed) parse();
        return sipVersion;
    }

    /**
     * Creates and returns a deep copy of the Message. This methods must ensure a
     * deep copy of the message, so that it can be modified without effecting
     * the original message. This provides useful functionality for proxying
     * Requests and Responses, for example:
     * <ul>
     * <li>Recieve a message.
     * <li>Create a deep clone of the message.
     * <li>Modify necessary headers.
     * <li>Proxy the message using the send methods on the SipProvider.
     * </ul>
     * The message contents are cloned as follows:
     * <ul>
     * <li>If the content is of type byte[] a new byte[] array is allocated and the
     * original contents are copied over to the cloned Message.
     * <li>If the content is of type String then a new String equal to the old
     * String is allocated and assigned to the cloned Message.
     * <li>If the content is of type Object and it has a public clone method
     * then it is invoked and the resultant Object is used in the new cloned
     * Message.
     * </ul>
     *
     * @return a deep copy of Message
     */
    public abstract Object clone();

    /**
     * This operation assumes that the message is already fully parsed.
     *
     * @param message
     */
    protected void cloneHeaders(SIPMessage message) {
        for (Map.Entry<String, LinkedList<Header>> entry : this.headers.entrySet()) {
            final LinkedList<Header> headerList = new LinkedList<Header>();
            for (Header header : entry.getValue())
                headerList.add((Header) header.clone());
            message.headers.put(entry.getKey(), headerList);
        }
        message.headerNames.addAll(this.headerNames);
        if (this.unrecognizedHeaders != null)
            message.unrecognizedHeaders = new LinkedList<ByteData>(this.unrecognizedHeaders);
    }

    /**
     * This operation assumes that the message is already fully parsed.
     *
     * @param message
     */
    protected void cloneContent(SIPMessage message) {
        if (this.content != null) {
            if (this.content instanceof byte[]) {
                final byte[] byteArray = (byte[]) this.content;
                final byte[] result = new byte[byteArray.length];
                System.arraycopy(byteArray, 0, result, 0, byteArray.length);
                message.content = result;
            } else if (this.content instanceof String) {
                message.content = new String((String) this.content);
            } else {
                Method cloneMethod;
                try {
                    cloneMethod = this.content.getClass().getMethod("clone");
                } catch (NoSuchMethodException e) {
                    if (logger.isDebugEnabled())
                        logger.debug("Cannot clone the contents of the SIP message: there is no clone method available for the object: " + this.content);
                    return;
                }
                try {
                    message.content = cloneMethod.invoke(this.content);
                } catch (Exception e) {
                    logger.warn("Cannot clone the contents of the SIP message, the contents will be copied instead.", e);
                    message.content = this.content;
                }
            }
        }
    }

    /**
     * This operation assumes that the message is already fully parsed.
     *
     * @param message
     */
    protected void cloneInternals(SIPMessage message) {
        message.sipVersion = this.sipVersion;
    }

    protected void recalculateContentLength() {
        if (!parsed) parse();
        int length = 0;
        if (this.content instanceof byte[])
            length = ((byte[]) this.content).length;
        else if (this.content instanceof String)
            length = ((String) this.content).length();
        ContentLengthHeader clHeader = getContentLength();
        if (clHeader == null) {
            clHeader = new ContentLengthHeaderImpl();
            try {
                clHeader.setContentLength(length);
                setContentLength(clHeader);
            } catch (InvalidArgumentException e) {
                logger.error("Cannot set content length!", e);
            }
        } else {
            try {
                clHeader.setContentLength(length);
            } catch (InvalidArgumentException e) {
                logger.error("Cannot set content length!", e);
            }
        }
        invalidateMessageData();
    }

    protected void buildMessageHeaders(StringBuilder sb) {
        //if (!parsed) doParse(); //It will be executed by the next method call.
        recalculateContentLength();
        for (String headerName : headerNames) {
            final LinkedList<Header> headerList = headers.get(headerName);
            for (Header header : headerList)
                sb.append(header).append('\r').append('\n');
        }
    }

    protected void buildMessageContent(StringBuilder sb) {
        if (!parsed) parse();
        //todo handle byte array
        if (this.content != null)
            sb.append(this.content);
    }

    protected void invalidateMessageData() {
        messageData = null;
    }

    /**
     * This method parses the {@link #messageData}. It is obliged to reset {@link #parsed} indicator.
     */
    protected abstract void parse();

    public String toString() {
        if (messageData == null)
            messageData = buildMessageData();
        return new String(messageData);
    }

    protected abstract byte[] buildMessageData();
}
/*
 * $Log$
 */